﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;

namespace Boggle
{
    public class BoggleGrid : IBoggleGrid, INotifyPropertyChanged
	{
		private static readonly Random random;

        private readonly Int32 gridWidth;

        private readonly Int32 gridHeight;

		private Die[,] dice;

        /// <summary>
        /// Gets the width of the grid.
        /// </summary>
        public Int32 GridWidth { get { return gridWidth; } }

        /// <summary>
        /// Gets the height of the grid.
        /// </summary>
        public Int32 GridHeight { get { return gridHeight; } }

		static BoggleGrid()
		{
			random = new Random();
		}

		public BoggleGrid(Die[,] _dice)
		{
			if (_dice == null)
			{
				throw new ArgumentNullException("_dice");
			}
			if (_dice.Length < 1)
			{
				throw new ArgumentException("_dice does not contain any dice");
			}
            
            _dice.ForEach(die => { if (die == null) throw new ArgumentException("_dice contains a null Die"); });
            gridWidth = _dice.GetLength(1);
            gridHeight = _dice.GetLength(0);
            dice = _dice;
		}

		public void Shuffle()
		{
			List<Die> tempDice = new List<Die>(gridWidth * gridHeight);
			Die[,] newDice = new Die[gridHeight, gridWidth];
			
			foreach (Die d in dice)
			{
				tempDice.Add(d);
			}

            for (int x = 0; x < gridWidth; x++)
			{
				for (int y = 0; y < gridHeight; y++)
                {
					Die d = tempDice[random.Next(tempDice.Count)];
					d.Roll();
					newDice[y, x] = d;
					tempDice.Remove(d);
				}
			}

			dice = newDice;
			OnPropertyChanged(new PropertyChangedEventArgs("Dice"));
		}

		public override string ToString()
		{
			StringBuilder sb = new StringBuilder();
            
            for (int y = 0; y < gridHeight; y++)
			{
                for (int x = 0; x < gridWidth; x++)
                {
					sb.Append(dice[y, x].CurrentValue.PadRight(4));
				}

				sb.Append(Environment.NewLine);
			}

			return sb.ToString();
		}

		public List<Die> ContainsWord(string word)
		{
			if (word == null)
			{
				return null;
			}
			if (word.Length < 1)
			{
				return new List<Die>(0);
			}

			List<Die> starts = GetStartDice(word);

			if (starts.Count < 1)
			{
				return null;
			}

			foreach (Die start in starts)
			{
				List<Die> wordDice = FindWord(new List<Die>(){start}, start, word.Substring(start.CurrentValue.Length));
				
				if (wordDice != null)
				{
					return wordDice;
				}
			}

			return null;
		}

		private List<Die> GetStartDice(string word)
		{
			List<Die> startDice = new List<Die>(word.Length);

			foreach (Die start in dice)
			{
				if (start.CurrentValue.Equals(TryRemove(word, start.CurrentValue.Length)))
				{
					startDice.Add(start);
				}
			}

			return startDice;
		}

		private void GetIndex(Die d, out int x, out int y)
		{
			for (x = 0; x < gridWidth; x++)
			{
				for (y = 0; y < gridHeight; y++)
				{
					if (dice[y, x].Equals(d))
					{
						return;
					}
				}
			}

			x = -1;
			y = -1;
			return;
		}

		private Die GetDiceAtIndex(int x, int y)
		{
			if(x < 0 || x >= gridWidth || y < 0 || y >= gridHeight)
			{
				return null;
			}

			return dice[y, x];
		}

		private List<Die> GetDiceSurroundingDie(Die d)
		{
			if (d == null)
			{
				throw new ArgumentNullException("d");
			}

			int x, y;
			GetIndex(d, out x, out y);

			if (x < 0 || y < 0)
			{
				throw new ArgumentException("d is not contained in this Grid");
			}

			List<Die> ds = new List<Die>()
			{
				GetDiceAtIndex(x - 1, y - 1),
				GetDiceAtIndex(x, y - 1),
				GetDiceAtIndex(x + 1, y - 1),
				GetDiceAtIndex(x - 1, y),
				GetDiceAtIndex(x + 1, y),
				GetDiceAtIndex(x - 1, y + 1),
				GetDiceAtIndex(x, y + 1),
				GetDiceAtIndex(x + 1, y + 1),
			};

			ds.RemoveAll(dc => dc == null);
			return ds;
		}

		private List<Die> FindWord(List<Die> used, Die d, string word)
		{
			if (used == null)
			{
				throw new ArgumentNullException("used");
			}
			if (d == null)
			{
				throw new ArgumentNullException("d");
			}
			if (word == null)
			{
				throw new ArgumentNullException("word");
			}
			if (word.Length == 0)
			{
				return used;
			}

			List<Die> nexts = GetDiceSurroundingDie(d);
			
			foreach (Die next in nexts)
			{
				string nextLetter = TryRemove(word, next.CurrentValue.Length);

				if (used.Contains(next) || !next.CurrentValue.Equals(nextLetter))
				{
					continue;
				}

				used.Add(next);
				string restOfWord = word.Substring(next.CurrentValue.Length);

				if (FindWord(used, next, restOfWord) == null)
				{
					used.Remove(next);
					continue;
				}

				return used;
			}

			return null;
		}

		private string TryRemove(string word, int startIndex)
		{
			if (startIndex >= word.Length)
			{
				return word;
			}

			return word.Remove(startIndex);
		}

        public List<Die> Dice 
        { 
            get 
            { 
                List<Die> dice = new List<Die>(gridWidth * gridHeight);
                this.dice.ForEach(d => dice.Add(d)); 
                return dice;
            }
        }

		#region INotifyPropertyChanged Implementation

		public event PropertyChangedEventHandler PropertyChanged;

		private void OnPropertyChanged(PropertyChangedEventArgs e)
		{
			var handler = PropertyChanged;

			if (handler != null)
			{
				handler(this, e);
			}
		}

		#endregion
    }
}
